package org.kernelab.numeric.test;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import org.kernelab.basis.Tools;
import org.kernelab.basis.Variable;
import org.kernelab.numeric.Tabulation;
import org.kernelab.numeric.matrix.Position;
import org.kernelab.numeric.matrix.Size;
import org.kernelab.numeric.matrix.Sized;

public abstract class InfiniteMatrix<E> implements Tabulation<E>
{
	protected class Block
	{
		protected RandomAccessFile	file;

		protected FileChannel		channel;

		protected long				capacity;

		protected TreeMap<Long, E>	cache;

		protected int				cacheLimit;

		protected ByteBuffer		buffer;

		public Block(RandomAccessFile file, int cacheLimit) throws IOException
		{
			this.file = file;
			this.channel = this.file.getChannel();
			this.capacity = this.channel.size() / elementBytes;
			this.cache = new TreeMap<Long, E>();
			this.cacheLimit = cacheLimit;
			this.buffer = ByteBuffer.allocateDirect(elementBytes);
		}

		protected void checkFlush()
		{
			if (cache.size() >= cacheLimit) {
				try {
					flush();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		protected void flush() throws IOException
		{
			for (Entry<Long, E> entry : cache.entrySet()) {
				flush(entry.getKey(), entry.getValue());
			}
			cache.clear();
		}

		protected void flush(long index, E element) throws IOException
		{
			buffer.rewind();
			InfiniteMatrix.this.write(element, buffer);
			buffer.rewind();
			channel.write(buffer, index);
		}

		protected ByteBuffer getBuffer()
		{
			return buffer;
		}

		protected TreeMap<Long, E> getCache()
		{
			return cache;
		}

		public int getCacheLimit()
		{
			return cacheLimit;
		}

		public long getCapacity()
		{
			return capacity;
		}

		public FileChannel getChannel()
		{
			return channel;
		}

		public RandomAccessFile getFile()
		{
			return file;
		}

		protected E read(long index) throws IOException
		{
			E element = cache.get(index);

			if (element == null) {
				buffer.rewind();
				channel.read(buffer, index);
				buffer.rewind();
				element = InfiniteMatrix.this.read(buffer);
			}

			return element;
		}

		protected void setBuffer(ByteBuffer buffer)
		{
			this.buffer = buffer;
		}

		public void setCacheLimit(int bufferSize)
		{
			this.cacheLimit = bufferSize;
		}

		protected void write(E element, long index) throws IOException
		{
			cache.put(index, element);
			checkFlush();
		}
	}

	protected class BlockHandler implements Runnable
	{
		protected Block			block;

		protected ByteBuffer[]	buffers;

		public BlockHandler(Block block, int buffers)
		{
			this.block = block;
			this.buffers = new ByteBuffer[Math.max(buffers, 1)];
			for (int i = 0; i < this.buffers.length; i++) {
				this.buffers[i] = ByteBuffer.allocateDirect(elementBytes);
				this.buffers[i].clear();
			}
		}

		@Override
		public void run()
		{
			try {
				block.channel.position(0);
				int activating = 0;
				for (int index = 0; index < block.capacity; index++) {
					buffers[activating++].rewind();
					if (activating == buffers.length) {
						block.channel.write(buffers);
						activating = 0;
					}
				}
				block.channel.write(buffers, 0, activating);

			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		public void setAll(E element)
		{
			for (ByteBuffer buffer : buffers) {
				buffer.rewind();
				write(element, buffer);
			}
		}

	}

	public static class DoubleInfiniteMatrix extends InfiniteMatrix<Double>
	{

		public DoubleInfiniteMatrix(Sized size, File file) throws IOException
		{
			super(size, Double.SIZE / Byte.SIZE, file);
		}

		@Override
		protected Double read(ByteBuffer buffer)
		{
			return buffer.getDouble();
		}

		@Override
		protected void write(Double element, ByteBuffer buffer)
		{
			if (element == null) {
				element = Double.NaN;
			}
			buffer.putDouble(element.doubleValue());
		}

	}

	public static int	BlockBitsSuperior		= 21;

	public static int	BlockBitsInferior		= 16;

	/**
	 * This is the number of maximum BlockHandler should be active at the same
	 * time. It is recommended that the number should be NO more than the CPU
	 * number.
	 */
	public static int	BlockHandlersSuperior	= 2;

	/**
	 * This is the size of buffer for each block by which we can write much more
	 * data each time. But it would NOT always take better effect with the size
	 * becoming larger.
	 */
	public static int	BlockBuffersSize		= 40;

	/**
	 * The directory where default cache file would be created.
	 */
	public static File	CacheDirectory			= new File("F:/cache/");

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		try {
			int size = 5000;
			DoubleInfiniteMatrix matrix = new DoubleInfiniteMatrix(new Size(size, size),
					new File("F:/cache/infinite"));
			matrix.setAll(0.0);
			// profileGetAndSet(matrix, 2000000);

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static final void printProfile(double bytes, double during)
	{
		bytes /= 1024 * 1024;
		Tools.debug("数据：" + Variable.numberFormatString(bytes, "0.00") + " Mb");
		during /= 1000;
		Tools.debug("用时：" + Variable.numberFormatString(during, "0.00") + " sec");
		Tools.debug("速率：" + Variable.numberFormatString(bytes / during, "0.00") + " M/s");
	}

	public static void profileGetAndSet(DoubleInfiniteMatrix matrix, long amount)
	{
		int rows = matrix.getRows();
		int columns = matrix.getColumns();

		Tools.debug(">>>>>>>>读/写 各测试" + amount + "次<<<<<<<<");

		long start = 0L;
		double during = 0.0;

		Tools.debug(">>>>>>>>读取性能分析>>>>>>>>");
		start = Tools.getTimeStamp();
		// Read
		for (int i = 0; i < amount; i++) {
			matrix.get((int) (Math.random() * rows), (int) (Math.random() * columns));
		}
		during = Tools.getTimeStamp() - start;
		printProfile(amount * 8.0, during);
		Tools.debug("<<<<<<<<读取性能分析<<<<<<<<");

		Tools.debug(">>>>>>>>写入性能分析>>>>>>>>");
		start = Tools.getTimeStamp();
		// Write
		for (int i = 0; i < amount; i++) {
			matrix.set(Math.random(), (int) (Math.random() * rows),
					(int) (Math.random() * columns));
		}
		during = Tools.getTimeStamp() - start;
		printProfile(amount * 8.0, during);
		Tools.debug("<<<<<<<<写入性能分析<<<<<<<<");
	}

	private int							rows;

	private int							columns;

	private long						total;

	private int							elementBytes;

	private File						directory;

	private int							blockBits;

	private long						blockCapacity;

	private long						blockCapacityRange;

	private Map<Integer, Block>			blocks;

	private Map<Integer, BlockHandler>	handlers;

	private LinkedList<Thread>			tasks;

	public InfiniteMatrix(Sized size, int elementBytes, File directory)
			throws IOException
	{
		this.setSize(size);

		this.setElementBytes(elementBytes);

		this.setDirectory(directory);

		this.setBlocks(BlockBitsSuperior);
	}

	protected RandomAccessFile createFile(int part) throws IOException
	{
		return new RandomAccessFile(directory.getAbsolutePath() + File.separator + part
				+ ".cache", "rw");
	}

	@Override
	public E get(int row, int column)
	{
		// try {
		// buffer.rewind();
		// locateBlock(row, column).getChannel().read(buffer);
		// } catch (IOException e) {
		// e.printStackTrace();
		// }
		// buffer.rewind();
		// return read(buffer);

		long index = index(row, column);
		Block block = locateBlock(index);
		E element = null;
		try {
			element = block.read((index & blockCapacityRange) * elementBytes);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return element;
	}

	protected long getBlockCapacity()
	{
		return blockCapacity;
	}

	// private void locateCache(int row, int column)
	// {
	// try {
	// long location = index(row, column) * elementBytes;
	// long position = cache.getFilePointer();
	//
	// if (location < position) {
	//
	// cache.seek(location);
	//
	// } else {
	//
	// long remain = location - position;
	// while (remain > 0) {
	// remain -= cache.skipBytes((int) Math.min(remain, BufferSize));
	// }
	//
	// }
	//
	// } catch (IOException e) {
	// e.printStackTrace();
	// }
	// }

	@Override
	public int getColumns()
	{
		return columns;
	}

	protected File getDirectory()
	{
		return directory;
	}

	public int getElementBytes()
	{
		return elementBytes;
	}

	protected Map<Integer, BlockHandler> getHandlers()
	{
		return handlers;
	}

	@Override
	public int getRows()
	{
		return rows;
	}

	protected LinkedList<Thread> getTasks()
	{
		return tasks;
	}

	public long getTotal()
	{
		return total;
	}

	private long index(int row, int column) throws ArrayIndexOutOfBoundsException
	{
		if (row < 0 || column < 0 || row >= rows || column >= columns) {
			throw new ArrayIndexOutOfBoundsException("(" + row + "," + column + ")"
					+ "\t@\t" + "(" + rows + "," + columns + ")");
		} else {
			return (long) row * (long) columns + column;
		}
	}

	protected Block locateBlock(long index)
	{
		// FileChannel channel = channels[(int) (index / blockCapacity)];

		// try {
		// // channel.position(index % blockCapacity * elementBytes);
		// block.channel.position((index & blockCapacityRange) * elementBytes);
		//
		// // Tools.debug((int) (index / channelCapacity) + ": " + row + "," +
		// // column);
		// } catch (IOException e) {
		// e.printStackTrace();
		// }

		return blocks.get((int) (index >> blockBits));
	}

	/**
	 * To read an element from a rewinded ByteBuffer.
	 * 
	 * @param buffer
	 *            The ByteBuffer which could be used to hold the bytes of the
	 *            element. The ByteBuffer has been already rewinded.
	 */
	protected abstract E read(ByteBuffer buffer);

	@Override
	public void set(E element, int row, int column)
	{
		// buffer.rewind();
		// write(element, buffer);
		// try {
		// buffer.rewind();
		// locateBlock(row, column).getChannel().write(buffer);
		// } catch (IOException e) {
		// e.printStackTrace();
		// }

		long index = index(row, column);

		Block block = locateBlock(index);

		try {
			block.write(element, (index & blockCapacityRange) * elementBytes);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void setAll(E element) throws IOException
	{
		long start = Tools.getTimeStamp();

		tasks.clear();

		for (BlockHandler handler : handlers.values()) {
			handler.setAll(element);
			Thread thread = new Thread(handler);
			thread.start();
			tasks.add(thread);
			if (tasks.size() >= BlockHandlersSuperior) {
				try {
					tasks.poll().join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		double during = Tools.getTimeStamp() - start;

		Tools.debug(">>>>>>>性能分析>>>>>>>");
		printProfile(getTotal() * elementBytes, during);
		Tools.debug("线程：" + BlockHandlersSuperior);
		Tools.debug("缓冲：" + BlockBuffersSize);
		Tools.debug("<<<<<<<性能分析<<<<<<<");
	}

	public void setBlocks(int blockBits) throws IOException
	{
		this.blockBits = Math.min(Math.max(blockBits, BlockBitsInferior),
				BlockBitsSuperior);

		// this.blockCapacity = Math.min(blockCapacity * elementBytes,
		// ChannelSizeSuperior)
		// / elementBytes;

		this.blockCapacity = 1L << this.blockBits;
		this.blockCapacityRange = this.blockCapacity - 1;

		long remain = getTotal();

		// blocks = new Block[(int) ((remain - 1) / this.blockCapacity) + 1];
		// blocks = new Block[(int) ((remain - 1) >> this.blockBits) + 1];
		int blocksNumber = (int) ((remain - 1) >> this.blockBits) + 1;
		blocks = new HashMap<Integer, Block>();
		handlers = new HashMap<Integer, BlockHandler>();

		for (int i = 0; i < blocksNumber; i++) {
			RandomAccessFile file = createFile(i);
			file.setLength(Math.min(this.blockCapacity, remain) * elementBytes);
			remain -= this.blockCapacity;
			// Block block = new Block(file, Math.max(BlockBuffersSize, 1));
			Block block = new Block(file, (int) (0.01 * this.blockCapacity));
			blocks.put(i, block);
			// channels[i] = blocks[i].getChannel();
			handlers.put(i, new BlockHandler(block, Math.max(BlockBuffersSize, 1)));
		}

		this.setTasks(new LinkedList<Thread>());
	}

	protected void setDirectory(File directory) throws IOException
	{
		if (directory == null) {
			if (!CacheDirectory.exists()) {
				CacheDirectory.mkdirs();
			}
			directory = CacheDirectory;
		}

		// if (directory.isDirectory()) {
		// if (!directory.exists()) {
		// directory.mkdirs();
		// }
		// directory = File.createTempFile("infinite", ".cache", directory);
		// directory.deleteOnExit();
		// } else {
		// if (!directory.getParentFile().exists()) {
		// directory.getParentFile().mkdirs();
		// }
		// }

		if (!directory.exists() || !directory.isDirectory()) {
			directory.mkdirs();
		}

		this.directory = directory;
	}

	private void setElementBytes(int elementBytes)
	{
		if (directory == null) {
			this.elementBytes = elementBytes;
		}
	}

	protected void setSize(Sized size)
	{
		rows = size.getRows();
		columns = size.getColumns();
		total = (long) rows * (long) columns;
	}

	protected void setTasks(LinkedList<Thread> tasks)
	{
		this.tasks = tasks;
	}

	public void synchronizeBlocks()
	{
		try {
			for (Block block : blocks.values()) {
				block.flush();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public String toString()
	{
		StringBuilder builder = new StringBuilder();

		for (int row = Position.FIRST; row < rows; row++) {
			for (int column = Position.FIRST; column < columns; column++) {
				E element = this.get(row, column);
				builder.append(element);
				builder.append('\t');
			}
			builder.append('\n');
		}

		return builder.toString();
	}

	/**
	 * To write a certain element to a rewinded ByteBuffer
	 * 
	 * @param element
	 *            The element to be written. Attention that the element might be
	 *            null for some cases.
	 * @param buffer
	 *            The ByteBuffer which could be used to hold the bytes of the
	 *            element. The ByteBuffer has been already rewinded.
	 */
	protected abstract void write(E element, ByteBuffer buffer);

}
